-- Lua I2P library
-- MIT License, Viktor Villainov 2019
--
local socket = require("socket")
local mime = require("mime")
local Sha = require("lib.sha2_256")
local basexx = require("lib.basexx")

-- idk like bytestream or smething
local Stream = {};


Stream.fromString = function(t)
	local i=0;
	return function()
		i=i+1;
		if(i <= string.len(t)) then
			return string.byte(t,i);
		else
			return nil;
		end
	end
end


Stream.toString = function(stream)
	local array = {};

	for i = 1, #stream, 1 do
		array[i] = string.char(stream[i]);
	end

	return table.concat(array,"");
end

-- string.translate characters function
function translate(t, from, to)
	for i = 1, #from do
		t = string.gsub(t, from:sub(i,i), to:sub(i,i))
		i = i + 1
	end
	return t
end

-- SAM helpers
function ok(r)
	return r.RESULT == "OK"
end

function sam_error(r)
	return string.format("SAM error: %s", r.RESULT)
end

-- Destination object
local Destination = {}

function Destination:new(text, private_key)
	local o = {}
	setmetatable(o, {__index = self})
	o.text = text

	if private_key == true then
		o.private_key = mooni2p.unb64(text)
		o.cert_len = (string.unpack(">H", string.sub(o.private_key, 386, 387)))
		o.public_key = string.sub(o.private_key, 1, 387+o.cert_len)
	else
		o.public_key = mooni2p.unb64(text)
	end

	return o
end

function Destination:base32()
	if self._base32 == nil then
		local desthash = Sha()
		desthash.update(Stream.fromString(self.public_key))
		desthash.finish()

		self._base32 = basexx.to_base32(Stream.toString(desthash.asBytes())):sub(1,52):lower()
		return self._base32
	else
		return self._base32
	end
end

function Destination:base64(private)
	if private == true then
		return mooni2p.b64(self.private_key)
	else
		return mooni2p.b64(self.public_key)
	end
end


--
-- MoonI2P
--


local mooni2p = { DEFAULT_ADDRESS = "127.0.0.1:7656", 
DEFAULT_DEST_TYPE = "7", Destination = Destination }

-- string.split function
function mooni2p._split(inputstr, sep)
	if sep == nil then
		sep = "%s"
	end
	local t={}
	for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
		table.insert(t, str)
	end
	return t
end

function mooni2p.get_sam_address()
	local addr_table = mooni2p._split(os.getenv("MOON_SAM_ADDRESS") or mooni2p.DEFAULT_ADDRESS, ":")

	return addr_table[1], tonumber(addr_table[2])
end

-- I2P version of base64 encoding
function mooni2p.b64(t)
	local res = (mime.b64(t))
	res = translate(res, "+/", "-~")
	return res
end

-- I2P version of base64 decoding
function mooni2p.unb64(t)
	local res = translate(t, "-~", "+/")
	res = (mime.unb64(res))
	return res
end

-- Parse reply from SAM API
function mooni2p.parse_sam_message(t)
	if t == nil then
		error("connection failed")
	end 
	local res = {}
	for w, _ in string.gmatch(t, "([^ ]+)") do
		local eqpos = w:find("=")
		if eqpos ~= nil then
			res[w:sub(1, eqpos-1)] = w:sub(eqpos+1, #w)
		end
	end
	return res
end

-- SAM commands
function mooni2p._hello(min_version, max_version)
	return string.format("HELLO VERSION MIN=%s MAX=%s\n", 
	min_version or "3.1", max_version or "3.1")
end

function mooni2p._session_create(style, session_id, destination, options)
	return string.format("SESSION CREATE STYLE=%s ID=%s DESTINATION=%s %s\n",
	style, session_id, destination, options or "")
end

function mooni2p._stream_connect(session_id, destination, silent)
	return string.format("STREAM CONNECT ID=%s DESTINATION=%s SILENT=%s\n", 
	session_id, destination, silent or "false")
end

function mooni2p._stream_accept(session_id, silent)
	return string.format("STREAM ACCEPT ID=%s SILENT=%s\n", 
	session_id, silent or "false")
end

function mooni2p._stream_forward(session_id, port, options)
	return string.format("STREAM FORWARD ID=%s PORT=%s %s\n", 
	session_id, port, options or "")
end

function mooni2p._naming_lookup(name)
	return string.format("NAMING LOOKUP NAME=%s\n", name)
end

function mooni2p._dest_generate(signature_type)
	return string.format("DEST GENERATE SIGNATURE_TYPE=%s\n", signature_type or "7")
end

-- Network connections
function mooni2p._get_socket(server, port)
	if server == nil then
		server, port = mooni2p.get_sam_address()
	end
	local sock = socket.tcp()
	assert(sock:connect(server, port))
	sock:send(mooni2p._hello())
	local reply = mooni2p.parse_sam_message(sock:receive("*l"))

	if ok(reply) then
		return sock
	else
		return false, sam_error(reply)
	end
end

function mooni2p.dest_lookup(name, server, port)
	local sock = assert(mooni2p._get_socket(server, port))
	sock:send(mooni2p._naming_lookup(name))
	local reply = mooni2p.parse_sam_message(sock:receive("*l"))
	sock:close()

	if ok(reply) then
		return mooni2p.Destination:new(reply.VALUE)
	else
		return false, sam_error(reply)
	end
end

function mooni2p.new_destination(server, port, dest_type)
	local sock = assert(mooni2p._get_socket(server, port))
	sock:send(mooni2p._dest_generate(dest_type))
	local reply = mooni2p.parse_sam_message(sock:receive("*l"))
	sock:close()

	return mooni2p.Destination:new(reply.PRIV, true)
end

function mooni2p.create_session(name, destination, server, port, style, options)
	if style == nil then style = "STREAM" end

	if destination == nil then 
		dest_string = "TRANSIENT"
	elseif type(destination) == "table" then
		dest_string = destination:base64(true)
	else
		dest_string = destination
	end

	local sock = assert(mooni2p._get_socket(server, port))
	sock:send(mooni2p._session_create(style, name, dest_string, options))
	local reply = mooni2p.parse_sam_message(sock:receive("*l"))

	if ok(reply) then
		if dest_string == "TRANSIENT" then
			destination = mooni2p.Destination:new(reply.DESTINATION, true)
		end
		return sock
	else
		sock:close()
		return false, sam_error(reply)
	end
end

function mooni2p.stream_connect(session_name, destination, server, port)
	if type(destination) == "string" and destination:sub(#destination-3) ~= ".i2p" then
		destination = mooni2p.Destination:new(destination)
	elseif type(destination) == "string" then
		destination = assert(mooni2p.dest_lookup(destination, server, port))
	end

	local sock = assert(mooni2p._get_socket(server, port))
	sock:send(mooni2p._stream_connect(session_name, destination:base64(), "false"))
	local reply = mooni2p.parse_sam_message(sock:receive("*l"))

	if ok(reply) then
		return sock
	else
		sock:close()
		return false, sam_error(reply)
	end
end

function mooni2p.stream_accept(session_name, server, port)
	local sock = assert(mooni2p._get_socket(server, port))
	sock:send(mooni2p._stream_accept(session_name, "false"))
	local reply = mooni2p.parse_sam_message(sock:receive("*l"))

	if ok(reply) then
		return sock
	else
		sock:close()
		return false, sam_error(reply)
	end
end


return mooni2p
